/*
Copyright 2023 the original author, Lam Tong

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package io.github.lamtong.msp.core.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.meilisearch.sdk.model.SearchResultPaginated;
import io.github.lamtong.msp.core.annotation.PrimaryKey;
import io.github.lamtong.msp.core.exception.MSPException;
import io.github.lamtong.msp.core.mapper.BaseMapper;
import io.github.lamtong.msp.core.pagination.Page;
import org.reflections.ReflectionUtils;
import org.reflections.util.ReflectionUtilsPredicates;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Utility class for generic types parameters about.
 *
 * @author Lam Tong
 * @version 1.0.0
 * @since 1.0.0.SNAPSHOT
 */
public final class BaseMapperUtils {

    /**
     * Acquires class of generic type of {@link BaseMapper} when extends it for given implementation class, generated by
     * default to implement {@code Mapper} interface, which is defined external by programmers and directly extends
     * {@link BaseMapper}.
     *
     * @param implClass implementation class for {@code Mapper} interface defined to extend {@link BaseMapper}
     * @return Actual class of generic type parameter of {@link BaseMapper} when defining {@code Mapper} interface
     */
    @SuppressWarnings(value = {"rawtypes"})
    public static Class<?> getActualTypeClass(Class<? extends BaseMapper> implClass) {
        Class<?> implClassInterface = implClass.getInterfaces()[0];
        Type genericType = Stream.of(implClassInterface.getGenericInterfaces())
                .filter(type -> type.getTypeName().startsWith(BaseMapper.class.getName()))
                .collect(Collectors.toList()).get(0);
        Type[] actualTypeArguments = ((ParameterizedTypeImpl) genericType).getActualTypeArguments();
        return (Class<?>) actualTypeArguments[0];
    }

    /**
     * Acquires name of primary key of given class under any one of cases as below:
     * <ul>
     *     <li>there exists only one field annotated with annotation {@link PrimaryKey}; or</li>
     *     <li>there exists only one field whose name ends with {@code id} in a case-insensitive case.</li>
     * </ul>
     * Due to that the entity class has been validated when acquiring instance of {@code Mapper} interface before, thus
     * there always exists one primary key of given class.<br/>
     * Field annotated with {@link PrimaryKey} is given priority, otherwise the latter case will be considered.
     *
     * @param clazz entity class
     * @return name of primary key
     */
    public static String getPrimaryKey(Class<?> clazz) {
        //noinspection unchecked
        Optional<Field> primaryKeyAnnoField = ReflectionUtils.getFields(clazz,
                        ReflectionUtilsPredicates.withAnnotation(PrimaryKey.class))
                .stream()
                .findAny();
        if (primaryKeyAnnoField.isPresent()) {
            return primaryKeyAnnoField.get().getName();
        }
        //noinspection unchecked
        List<Field> primaryKeyField = ReflectionUtils.getFields(clazz)
                .stream()
                .filter(field -> field.getName().toLowerCase().endsWith("id"))
                .collect(Collectors.toList());
        return primaryKeyField.get(0).getName();
    }

    /**
     * Acquires value of primary key for given object, which works as follows:
     * <ol>
     *     <li>acquires name of primary key;</li>
     *     <li>gets the method name to acquire value of primary key;</li>
     *     <li>gets method instance to acquire value of primary key;</li>
     *     <li>acquires value of primary key by reflection of {@code Java}</li>
     * </ol>
     *
     * @param o instance to acquire value of primary key
     * @return value of primary key in a {@link String}
     */
    public static String getPrimaryKeyValue(Object o) {
        String primaryKeyValue;
        String primaryKey = getPrimaryKey(o.getClass());
        String getPrimaryKeyMethod = "get".concat(String.valueOf(Character.toUpperCase(primaryKey.charAt(0))))
                .concat(primaryKey.substring(1));
        try {
            Method method = o.getClass().getMethod(getPrimaryKeyMethod);
            primaryKeyValue = String.valueOf(method.invoke(o));
        } catch (NoSuchMethodException e) {
            throw new MSPException(String.format("Primary key [%s] does not have a getter method, please check.",
                    primaryKey));
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new MSPException(e);
        }
        return primaryKeyValue;
    }

    /**
     * Converts an instance of {@link Map} into an instance of specified type.
     *
     * @param map   {@link Map} instance to be converted
     * @param clazz specified type
     * @param <T>   generic type parameter
     * @return instance of specified type
     */
    public static <T> T convertFromMapToType(Map<String, Object> map, Class<T> clazz) {
        T t;
        ObjectMapper mapper = new ObjectMapper();
        try {
            String s = mapper.writeValueAsString(map);
            t = mapper.readValue(s, clazz);
        } catch (JsonProcessingException e) {
            throw new MSPException(e);
        }
        return t;
    }

    /**
     * Acquires all fields' name for given {@link Class} instance in an array of {@link String}
     *
     * @param clazz {@link Class} instance
     * @param <T>   generic type parameter
     * @return fields' name in an array of {@link String}
     */
    @SuppressWarnings(value = {"unchecked"})
    public static <T> String[] getAllGenericTypeFields(Class<T> clazz) {
        return ReflectionUtils.getFields(clazz)
                .stream()
                .map(Field::getName)
                .toArray(String[]::new);
    }

    /**
     * Converts paginated search result into {@link Page}.<br/> When converting into {@link Page}, rules are as
     * follows:
     * <ol>
     *     <li>{@link SearchResultPaginated#hitsPerPage} => {@link Page#pageSize};</li>
     *     <li>{@link SearchResultPaginated#page} => {@link Page#pageNumber};</li>
     *     <li>{@link SearchResultPaginated#totalPages} => {@link Page#pages};</li>
     *     <li>{@link SearchResultPaginated#totalHits} => {@link Page#total}</li>
     *     <li>{@link SearchResultPaginated#hits} => {@link Page#list}</li>
     * </ol>
     *
     * @param result paginated search result to be converted
     * @param clazz  target class
     * @param <T>    generic type
     * @return paginated result in a {@link Page}
     * @see #convertFromMapToType(Map, Class)
     */
    public static <T> Page<T> convertToPage(SearchResultPaginated result, Class<T> clazz) {
        Page<T> page = new Page<>(result.getHitsPerPage(), result.getPage());
        page.setPages(result.getTotalPages());
        page.setTotal(result.getTotalHits());
        List<T> list = result.getHits()
                .stream()
                .map(map -> convertFromMapToType(map, clazz))
                .collect(Collectors.toList());
        page.setList(list);
        return page;
    }

}
